import merge from 'lodash.merge';
import { getRandomId, excludeKeys } from '@/utils/helper';

function getCallbackName(namespace) {
  return `${namespace.toUpperCase()}_CALLBACK_${getRandomId(16)}`;
}

function windowGuard() {
  if (typeof window === 'undefined') {
    throw new Error('window is not defined');
  }
}

function getAndroidRootInterface(def) {
  windowGuard();
  const error = name => {
    throw new Error(`no such interface '${name}'`);
  };
  if (def.rootInterface) {
    const root = window[def.rootInterface];
    if (!root) error(def.rootInterface);
    return root;
  }
  const root = window.LifeDetailInterface || window.LifeInterface;
  if (!root) error('LifeDetailInterface, LifeInterface');
  return root;
}

function getIosRootInterface() {
  windowGuard();
  const { webkit } = window;
  if (typeof webkit === 'undefined') {
    throw new Error('webkit is not attached to window');
  }
  return webkit.messageHandlers;
}

function isIos() {
  try {
    getIosRootInterface();
    return true;
  } catch (e) {
    return false;
  }
}

function getParamSerializer(def) {
  const { paramSerializer } = def;
  if (!paramSerializer) return (t, sender) => sender(t);
  if (Array.isArray(paramSerializer)) {
    return (options, sender) => sender(...paramSerializer.map(k => options[k]));
  }
  if (typeof paramSerializer === 'function') return paramSerializer;
  throw new Error(
    `paramSerializer with type of '${typeof paramSerializer}' not support`
  );
}

function makeGetSender(getRootSender) {
  return function getSender(getRoot, def) {
    const { interfaceMethod } = def;
    const paramSerializer = getParamSerializer(def);
    if (!interfaceMethod) {
      throw new Error(`no interfaceMethod defined in ${def.methodName}`);
    }
    let interfaceCaller = null;

    function getInterfaceCaller(root) {
      if (typeof interfaceMethod === 'function') {
        return interfaceMethod(root);
      }
      return (...rest) => getRootSender(root, interfaceMethod)(...rest);
    }

    function sender(options) {
      const root = getRoot(def);
      if (!interfaceCaller) interfaceCaller = getInterfaceCaller(root);
      return paramSerializer(
        merge({}, def.defaultParams, options),
        interfaceCaller
      );
    }

    return sender;
  };
}

const getIosSender = makeGetSender((r, n) => (...a) => r[n].postMessage(...a));
const getAndroidSender = makeGetSender((r, n) => (...a) => r[n](...a));

function getReceiver(def) {
  if (def.resultSerializer) return (...rest) => def.resultSerializer(...rest);
  return t => t;
}

function makePlatformInterface({ getRootInterface, getSender, callbackNs }) {
  function getAsyncSender(sender, receiver, callbackName) {
    return function asyncSender(options = {}) {
      const cbName = options.callbackName || callbackName;
      return new Promise(resolve => {
        window[cbName] = function bridgeCallback(...rest) {
          delete window[cbName];
          const result = receiver(...rest);
          resolve(result);
          if (options.callback) options.callback(result);
        };
        sender(merge({}, options, { callbackName: cbName }));
      });
    };
  }
  function getSyncSender(sender, receiver, callbackName) {
    return function syncSender(options = {}) {
      if (typeof options.callback === 'function') {
        const { callback } = options;
        window[callbackName] = function bridgeCallback(...rest) {
          callback(receiver(...rest));
        };
        try {
          sender(merge({}, options, { callbackName }));
        } catch(e) {} // eslint-disable-line
        return () => {
          delete window[callbackName];
        };
      }
      return receiver(sender(options));
    };
  }
  function makeSpecificPlatformInterface(def) {
    const sender = getSender(getRootInterface, def);
    const receiver = getReceiver(def);

    const callbackName = def.callbackName || getCallbackName(callbackNs);
    if (def.async) {
      return getAsyncSender(sender, receiver, callbackName);
    }
    return getSyncSender(sender, receiver, callbackName);
  }

  return makeSpecificPlatformInterface;
}

const generateIosInterface = makePlatformInterface({
  getRootInterface: getIosRootInterface,
  getSender: getIosSender,
  callbackNs: 'IOS'
});
const generateAndroidInterface = makePlatformInterface({
  getRootInterface: getAndroidRootInterface,
  getSender: getAndroidSender,
  callbackNs: 'ANDROID'
});

function getMergedDefinition(mergeKey) {
  const mergeKeys = Array.isArray(mergeKey) ? mergeKey : [mergeKey];
  return def =>
    mergeKeys.reduce(
      (o, k) => merge(o, def[k]),
      excludeKeys(def, ['methodName'])
    );
}

const getAndroidDefinition = getMergedDefinition('android');
const getIosDefinition = getMergedDefinition('ios');

function getMethodName(def) {
  return !def.async ? def.methodName : `${def.methodName}Async`;
}

function makeBridge(definition) {
  return definition.reduce(
    (o, def) => ({
      ...o,
      [getMethodName(def)]: isIos()
        ? generateIosInterface(getIosDefinition(def))
        : generateAndroidInterface(getAndroidDefinition(def))
    }),
    []
  );
}

export default makeBridge;
